package com.lipaluma.field.mapper.builder;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import com.lipaluma.context.Context;
import com.lipaluma.context.mapping.impl.BuilderMappingManager;
import com.lipaluma.exceptions.CsvParserException;
import com.lipaluma.field.adaptor.Adaptor;
import com.lipaluma.field.adaptor.builder.AdaptorBuilder;
import com.lipaluma.field.adaptor.builder.AdaptorOnEachValueBuilder;
import com.lipaluma.field.mapper.FieldAdapted;
import com.lipaluma.field.mapper.FieldAdaptedOnEachValue;
import com.lipaluma.field.mapper.FieldIdAssignable;
import com.lipaluma.field.mapper.FieldMapper;
import com.lipaluma.field.mapper.FieldMergeable;
import com.lipaluma.field.mapper.FieldValidated;
import com.lipaluma.field.mapper.FieldValidatedOnEachValue;
import com.lipaluma.field.mapper.factory.FieldMapperFactory;
import com.lipaluma.field.mapper.factory.MultipleFieldMapperFactory;
import com.lipaluma.field.mapper.initializer.FieldMapperInitializer;
import com.lipaluma.field.mapper.mapping.FieldMappingInfo;
import com.lipaluma.field.mapper.mapping.TargetMappingInfo;
import com.lipaluma.field.mapper.multiple.MultipleFieldMapper;
import com.lipaluma.field.mapper.single.impl.ObjectFieldMapper;
import com.lipaluma.field.mapper.single.impl.OneToManyFieldMapper;
import com.lipaluma.field.merge.FieldMergedAlgorithm;
import com.lipaluma.field.merge.FieldMergedFunction;
import com.lipaluma.field.merge.MergingTargetFunction;
import com.lipaluma.field.merge.TargetMergedAlgorithm;
import com.lipaluma.field.validator.Validator;
import com.lipaluma.field.validator.builder.ValidatorBuilder;
import com.lipaluma.field.validator.builder.ValidatorOnEachValueBuilder;
import com.lipaluma.util.ReflectionUtils;

public class FieldMapperBuilder implements ObjectFieldMapperAfterRegisterBuilder, FieldMapperAfterRegisterBuilder, NeedMappingBuilder, AddNewFieldMapperBuilder {
	protected FieldMapperBuilder parent;
	protected Class<?> targetClass;
	protected List<Association> associations = new ArrayList<Association>();
	protected Context context = new Context(new BuilderMappingManager());

	private boolean nextFieldInOneToMany = false;
	
	protected FieldMapperBuilder(Class<?> clazz) {
		this.targetClass = clazz;
	}
	protected FieldMapperBuilder(FieldMapperBuilder parent, Class<?> clazz) {
		this.targetClass = clazz;
		this.parent = parent;
	}
	
	public static FieldMapperBuilder forClass(Class<?> targetClass) {
		return new FieldMapperBuilder(targetClass);
	}
	
	@Override
	public FieldMapperAfterRegisterBuilder addFieldMapper(String fieldName, FieldMapper mapper) {
		Field field = ReflectionUtils.findField(targetClass, fieldName);
		if(field == null)
			throw new CsvParserException("Error on adding new FieldMapper : the field '"+fieldName+"' is not found on target class "+targetClass);
		if(ObjectFieldMapper.class.isAssignableFrom(mapper.getClass()))
			throw new CsvParserException("Error on setting one to many option to field"+fieldName+" : By Builder construction, the given field mapper "+mapper.getClass()+" must implements interface FieldOneToManyActivable");
		context.getMappingManagerAs(BuilderMappingManager.class).addMappings(field, mapper.getMappings());
		FieldMapper fieldMapper;
		if(nextFieldInOneToMany) {
			fieldMapper = new OneToManyFieldMapper(mapper);
			this.nextFieldInOneToMany = false;
		} else {
			fieldMapper = mapper;
		}
		
		if(FieldMapperInitializer.class.isAssignableFrom(fieldMapper.getClass()))
			((FieldMapperInitializer)fieldMapper).init(context, field);
		associations.add(new Association(field, mapper));
		return this;
	}

	@Override
	public NeedMappingBuilder mapFieldAutomatically(String fieldName) {
		Field field = ReflectionUtils.findField(targetClass, fieldName);
		if(field == null)
			throw new CsvParserException("Error on adding new FieldMapper : the field '"+fieldName+"' is not found on target "+targetClass);
		FieldMapper fieldMapper;
		if(nextFieldInOneToMany) {
			Class<?> clazz = ReflectionUtils.getParameterizedClass(field);
			FieldMapper internFieldMapper = FieldMapperFactory.createFieldMapperForType(clazz);
			if(ObjectFieldMapper.class.isAssignableFrom(internFieldMapper.getClass()))
				throw new CsvParserException("An Unknown Object was detected for the field "+field.getName()+" ("+field.getClass()+") during the automatic mapping. If the object has to be mapped, use the mapObject method instead.");
			fieldMapper = new OneToManyFieldMapper(internFieldMapper);
			this.nextFieldInOneToMany = false;
		} else {
			fieldMapper = FieldMapperFactory.createFieldMapperForType(field.getType());
			if(ObjectFieldMapper.class.isAssignableFrom(fieldMapper.getClass()))
				throw new CsvParserException("An Unknown Object was detected for the field "+field.getName()+" ("+field.getClass()+") during the automatic mapping. If the object has to be mapped, use the mapObject method instead.");
		}
		((FieldMapperInitializer)fieldMapper).init(context, field);
		associations.add(new Association(field, fieldMapper));
		return this;
	}
	
	@Override
	public NeedMappingBuilder mapMultipleFieldAutomatically(String fieldName) {
		Field field = ReflectionUtils.findField(targetClass, fieldName);
		if(field == null)
			throw new CsvParserException("Error on adding new FieldMapper : the field '"+fieldName+"' is not found on target "+targetClass);
		FieldMapper mapper;
		if(nextFieldInOneToMany) {
			Class<?> clazz = ReflectionUtils.getParameterizedClass(field);
			MultipleFieldMapper internFieldMapper = MultipleFieldMapperFactory.createMultipleFieldMapperForType(clazz);
			mapper = new OneToManyFieldMapper(internFieldMapper);
			this.nextFieldInOneToMany = false;
		} else {
			mapper = MultipleFieldMapperFactory.createMultipleFieldMapperForType(field.getType());
		}
		associations.add(new Association(field, mapper));
		return this;
	}

	@Override
	public AddNewFieldMapperBuilder mapNextFieldInOneToMany() {
		this.nextFieldInOneToMany = true;
		return this;
	}

	@Override
	public AddNewFieldMapperBuilder mapObject(String fieldName, Class<?> newTargetClass) {
		Field field = ReflectionUtils.findField(targetClass, fieldName);
		associations.add(new Association(field, null));
		return new FieldMapperBuilder(this, newTargetClass);
	}

	@Override
	public ObjectFieldMapperAfterRegisterBuilder endMappingObject() {
		FieldMapperBuilder.Association association = parent.getLastAssociation();
		List<FieldMapper> fieldMappersCreated = build();
		FieldMapper mapper = new ObjectFieldMapper(fieldMappersCreated);
		if(parent.nextFieldInOneToMany) {
			context.getMappingManagerAs(BuilderMappingManager.class).defineAsOneToMany(association.field);
			mapper = new OneToManyFieldMapper(mapper);
			parent.nextFieldInOneToMany = false;
		}
		((FieldMapperInitializer)mapper).init(context, association.field);
		association.fieldMapper = mapper;
		return parent;
	}

	@Override
	public ObjectFieldMapperAfterRegisterBuilder mapObject(String fieldName, List<FieldMapper> fieldMappers) {
		Field field = ReflectionUtils.findField(targetClass, fieldName);
		FieldMapper mapper = new ObjectFieldMapper(fieldMappers);
		if(nextFieldInOneToMany) {
			context.getMappingManagerAs(BuilderMappingManager.class).defineAsOneToMany(field);
			mapper = new OneToManyFieldMapper(mapper);
			nextFieldInOneToMany = false;
		}
		((FieldMapperInitializer)mapper).init(context, field);
		associations.add(new Association(field, mapper));
		return this;
	}

	@Override
	public ObjectFieldMapperAfterRegisterBuilder targetMergeable() {
		FieldMapperBuilder.Association association = parent.getLastAssociation();
		ObjectFieldMapper fieldMapper;
		if(OneToManyFieldMapper.class.isAssignableFrom(association.fieldMapper.getClass())) {
			fieldMapper = (ObjectFieldMapper)((OneToManyFieldMapper)association.fieldMapper).getFieldMapper();
		} else {
			fieldMapper = (ObjectFieldMapper)association.fieldMapper;
		}
		fieldMapper.setTargetCanBeMerged(true);
		return this;
	}

	@Override
	public ObjectFieldMapperAfterRegisterBuilder mergeTargetWith(TargetMergedAlgorithm algorithm, String... params) {
		FieldMapperBuilder.Association association = parent.getLastAssociation();
		ObjectFieldMapper fieldMapper;
		if(OneToManyFieldMapper.class.isAssignableFrom(association.fieldMapper.getClass())) {
			fieldMapper = (ObjectFieldMapper)((OneToManyFieldMapper)association.fieldMapper).getFieldMapper();
		} else {
			fieldMapper = (ObjectFieldMapper)association.fieldMapper;
		}		
		fieldMapper.setMergingTargetFunction(new MergingTargetFunction(algorithm, params));
		return this;
	}
	
	@Override
	public ObjectFieldMapperAfterRegisterBuilder addTargetMappingInfo(TargetMappingInfo targetMappingInfo) {
		FieldMapperBuilder.Association association = parent.getLastAssociation();
		ObjectFieldMapper fieldMapper;
		if(OneToManyFieldMapper.class.isAssignableFrom(association.fieldMapper.getClass())) {
			fieldMapper = (ObjectFieldMapper)((OneToManyFieldMapper)association.fieldMapper).getFieldMapper();
		} else {
			fieldMapper = (ObjectFieldMapper)association.fieldMapper;
		}		
		fieldMapper.setTargetMappingInfo(targetMappingInfo);
		return this;
	}

//	@Override
//	public FieldMapperBuilder mapObject(String fieldName, List<FieldMapper> fieldMappers, boolean targetMergeable) {
//		Field field = ReflectionUtils.findField(targetClass, fieldName);
//		FieldMapper mapper = new ObjectFieldMapper(fieldMappers);
//		((ObjectFieldMapper)mapper).setTargetCanBeMerged(targetMergeable);
//		if(nextFieldInOneToMany) {
//			context.getMappingManagerAs(BuilderMappingManager.class).defineAsOneToMany(field);
//			mapper = new OneToManyFieldMapper(mapper);
//			nextFieldInOneToMany = false;
//		}
//		((FieldMapperInitializer)mapper).init(context, field);
//		associations.add(new Association(field, mapper));
//		return this;
//	}
//
//	@Override
//	public FieldMapperBuilder mapObject(String fieldName, List<FieldMapper> fieldMappers, MergingTargetFunction mergingTargetFunction) {
//		Field field = ReflectionUtils.findField(targetClass, fieldName);
//		FieldMapper mapper = new ObjectFieldMapper(fieldMappers);
//		((ObjectFieldMapper)mapper).setMergingTargetFunction(mergingTargetFunction);
//		if(nextFieldInOneToMany) {
//			context.getMappingManagerAs(BuilderMappingManager.class).defineAsOneToMany(field);
//			mapper = new OneToManyFieldMapper(mapper);
//			nextFieldInOneToMany = false;
//		}
//		((FieldMapperInitializer)mapper).init(context, field);
//		associations.add(new Association(field, mapper));
//		return this;
//	}

	@Override
	public FieldMapperAfterRegisterBuilder mapOn(FieldMappingInfo... mappings) {
		Association association = getLastAssociation();
		initMappingsWithField(association.field, mappings);
		context.getMappingManagerAs(BuilderMappingManager.class).addMappings(association.field, mappings);

		if(FieldMapperInitializer.class.isAssignableFrom(association.fieldMapper.getClass()))
			((FieldMapperInitializer)association.fieldMapper).init(context, association.field);
		return this;
	}

	@Override
	public InitMappingBuilder map() {
		Association association = getLastAssociation();
		return new InitMappingBuilder(this, association.fieldMapper, association.field);
	}	

	public FieldMapperAfterRegisterBuilder asId() {
		Association association = getLastAssociation();
		if(!FieldIdAssignable.class.isAssignableFrom(association.fieldMapper.getClass()))
			throw new CsvParserException("Error on define the field mapper '"+association.fieldMapper.getField().getName()+"' as id by builder construction : The class "+association.fieldMapper.getClass()+" need to implements "+FieldIdAssignable.class);
		((FieldIdAssignable)association.fieldMapper).assignId(true);
		return this;
	}
	
	public FieldMapperAfterRegisterBuilder mergeWith(FieldMergedAlgorithm algorithm, String... params) {
		Association association = getLastAssociation();
		if(!FieldMergeable.class.isAssignableFrom(association.fieldMapper.getClass()))
			throw new CsvParserException("Error on define the field mapper '"+association.fieldMapper.getField().getName()+"' as mergeable by builder construction : The class "+association.fieldMapper.getClass()+" need to implements "+FieldMergeable.class);
		FieldMergedFunction mergeInfos = new FieldMergedFunction(algorithm, params);
		((FieldMergeable)association.fieldMapper).mergeWith(mergeInfos);
		return this;
	}

	public FieldMapperAfterRegisterBuilder mergeWith(FieldMergedAlgorithm algorithm) {
		Association association = getLastAssociation();
		if(!FieldMergeable.class.isAssignableFrom(association.fieldMapper.getClass()))
			throw new CsvParserException("Error on define the field mapper '"+association.fieldMapper.getField().getName()+"' as mergeable by builder construction : The class "+association.fieldMapper.getClass()+" need to implements "+FieldMergeable.class);
		((FieldMergeable)association.fieldMapper).mergeWith(new FieldMergedFunction(algorithm));
		return this;
	}

	public ValidatorBuilder validate() {
		FieldMapper fieldMapper = getLastAssociation().fieldMapper;
		if(!FieldValidated.class.isAssignableFrom(fieldMapper.getClass()))
			throw new CsvParserException("Error on validation of field mapper '"+fieldMapper.getField().getName()+"' : The class "+fieldMapper.getClass()+" doesn't implements "+FieldValidated.class);
		return new ValidatorBuilder(this, (FieldValidated)fieldMapper);
	}
	public ValidatorOnEachValueBuilder validateOnEachValue() {
		FieldMapper fieldMapper = getLastAssociation().fieldMapper;
		if(!FieldValidatedOnEachValue.class.isAssignableFrom(fieldMapper.getClass()))
			throw new CsvParserException("Error on validation of field mapper '"+fieldMapper.getField().getName()+"' : The class "+fieldMapper.getClass()+" doesn't implements "+FieldValidatedOnEachValue.class);
		return new ValidatorOnEachValueBuilder(this, (FieldValidatedOnEachValue)fieldMapper);
	}
	public AdaptorBuilder adapt() {
		FieldMapper fieldMapper = getLastAssociation().fieldMapper;
		if(!FieldAdapted.class.isAssignableFrom(fieldMapper.getClass()))
			throw new CsvParserException("Error on validation of field mapper '"+fieldMapper.getField().getName()+"' : The class "+fieldMapper.getClass()+" doesn't implements "+FieldAdapted.class);
		return new AdaptorBuilder(this, (FieldAdapted)fieldMapper);
	}
	public AdaptorOnEachValueBuilder adaptOnEachValue() {
		FieldMapper fieldMapper = getLastAssociation().fieldMapper;
		if(!FieldAdaptedOnEachValue.class.isAssignableFrom(fieldMapper.getClass()))
			throw new CsvParserException("Error on validation of field mapper '"+fieldMapper.getField().getName()+"' : The class "+fieldMapper.getClass()+" doesn't implements "+FieldAdaptedOnEachValue.class);
		return new AdaptorOnEachValueBuilder(this, (FieldAdaptedOnEachValue)fieldMapper);
	}
	
	public FieldMapperAfterRegisterBuilder validate(Validator... validators) {
		FieldMapper fieldMapper = getLastAssociation().fieldMapper;
		if(!FieldValidated.class.isAssignableFrom(fieldMapper.getClass()))
			throw new CsvParserException("Error on validation of field mapper '"+fieldMapper.getField().getName()+"' : The class "+fieldMapper.getClass()+" doesn't implements "+FieldValidated.class);
		if(validators == null)
			throw new CsvParserException("Error on validation of field mapper '"+fieldMapper.getField().getName()+"' : Array of given validators is null.");
		for (Validator validator : validators) {
			FieldValidated fieldValidated = (FieldValidated)fieldMapper;
			fieldValidated.addNewValidator(validator);
		}
		return this;
	}
	public FieldMapperAfterRegisterBuilder validateOnEachValue(Validator... validators) {
		FieldMapper fieldMapper = getLastAssociation().fieldMapper;
		if(!FieldValidatedOnEachValue.class.isAssignableFrom(fieldMapper.getClass()))
			throw new CsvParserException("Error on validation of field mapper '"+fieldMapper.getField().getName()+"' : The class "+fieldMapper.getClass()+" doesn't implements "+FieldValidatedOnEachValue.class);
		if(validators == null)
			throw new CsvParserException("Error on validation of field mapper '"+fieldMapper.getField().getName()+"' : Array of given validators is null.");
		for (Validator validator : validators) {
			FieldValidatedOnEachValue fieldValidatedOnEachValue = (FieldValidatedOnEachValue)fieldMapper;
			fieldValidatedOnEachValue.addNewValidatorOnEachValue(validator);
		}
		return this;
	}

	public FieldMapperAfterRegisterBuilder adapt(Adaptor... adaptors) {
		FieldMapper fieldMapper = getLastAssociation().fieldMapper;
		if(!FieldAdapted.class.isAssignableFrom(fieldMapper.getClass()))
			throw new CsvParserException("Error on setting adapters in field mapper '"+fieldMapper.getField().getName()+"' : The class "+fieldMapper.getClass()+" doesn't implements "+FieldAdapted.class);
		if(adaptors == null)
			throw new CsvParserException("Error on setting adapters in field mapper '"+fieldMapper.getField().getName()+"' : Array of given adaptors is null.");
		for (Adaptor adaptor : adaptors) {
			FieldAdapted fieldAdaptor = (FieldAdapted)fieldMapper;
			fieldAdaptor.addNewAdaptor(adaptor);
		}
		return this;
	}

	public FieldMapperAfterRegisterBuilder adaptOnEachValue(Adaptor... adaptors) {
		FieldMapper fieldMapper = getLastAssociation().fieldMapper;
		if(!FieldAdaptedOnEachValue.class.isAssignableFrom(fieldMapper.getClass()))
			throw new CsvParserException("Error on setting adapters in field mapper '"+fieldMapper.getField().getName()+"' : The class "+fieldMapper.getClass()+" doesn't implements "+FieldAdaptedOnEachValue.class);
		if(adaptors == null)
			throw new CsvParserException("Error on setting adapters in field mapper '"+fieldMapper.getField().getName()+"' : Array of given adaptors is null.");
		for (Adaptor adaptor : adaptors) {
			FieldAdaptedOnEachValue fieldAdaptedOnEachValue = (FieldAdaptedOnEachValue)fieldMapper;
			fieldAdaptedOnEachValue.addNewAdaptorOnEachValue(adaptor);
		}
		return this;
	}

	@Override
	public List<FieldMapper> build() {
		List<FieldMapper> result = new ArrayList<FieldMapper>();
		for(Association association : associations) {
			result.add(association.fieldMapper);
		}
		return result;
	}	

	public class Association {
		private Field field;
		private FieldMapper fieldMapper;
		public Association(Field field, FieldMapper fieldMapper) {
			this.field = field;
			this.fieldMapper = fieldMapper;
		}
		public Field getField() {
			return field;
		}
		public void setField(Field field) {
			this.field = field;
		}
		public FieldMapper getFieldMapper() {
			return fieldMapper;
		}
		public void setFieldMapper(FieldMapper fieldMapper) {
			this.fieldMapper = fieldMapper;
		}
	}

	public Association getLastAssociation() {
		return associations.get(associations.size() - 1);
	}
	
	public Context getContext() {
		return context;
	}

	static void initMappingsWithField(Field field, FieldMappingInfo... mappings) {
		for (int i = 0; i < mappings.length; i++) {
			if(mappings.length > 1)
				mappings[i].withFieldAndPosition(field, i);
			else 
				mappings[i].onField(field);
		}
	}
}
